Jelajahi hook useFormState React untuk validasi form sisi server dan manajemen state. Pelajari cara membangun form yang tangguh dan disempurnakan secara progresif dengan contoh praktis.
React useFormState: Kupas Tuntas Manajemen State dan Validasi Form Modern
Formulir adalah landasan interaktivitas web. Dari formulir kontak sederhana hingga wizard multi-langkah yang kompleks, mereka sangat penting untuk input pengguna dan pengiriman data. Selama bertahun-tahun, developer React telah menavigasi lanskap strategi manajemen state, mulai dari menangani komponen terkontrol secara manual dengan useState hingga memanfaatkan library pihak ketiga yang kuat seperti Formik dan React Hook Form. Meskipun solusi ini sangat baik, tim inti React telah memperkenalkan primitif baru yang kuat yang memikirkan kembali hubungan antara formulir dan server: hook useFormState.
Hook ini, yang diperkenalkan bersama React Server Actions, bukan sekadar alat manajemen state lainnya. Ini adalah bagian fundamental dari arsitektur yang lebih terintegrasi dan berpusat pada server yang memprioritaskan ketangguhan, pengalaman pengguna, dan sebuah konsep yang sering dibicarakan tetapi menantang untuk diimplementasikan: penyempurnaan progresif.
Dalam panduan komprehensif ini, kita akan menjelajahi setiap aspek dari useFormState. Kita akan mulai dari dasar-dasarnya, membandingkannya dengan metode tradisional, membangun contoh praktis, dan mendalami pola-pola canggih untuk validasi dan umpan balik pengguna. Pada akhirnya, Anda tidak hanya akan mengerti cara menggunakan hook ini, tetapi juga pergeseran paradigma yang diwakilinya dalam membangun formulir di aplikasi React modern.
Apa itu `useFormState` dan Mengapa Penting?
Pada intinya, useFormState adalah Hook React yang dirancang untuk mengelola state sebuah formulir berdasarkan hasil dari sebuah form action. Ini mungkin terdengar sederhana, tetapi kekuatan sebenarnya terletak pada desainnya, yang secara mulus mengintegrasikan pembaruan sisi klien dengan logika sisi server.
Pikirkan tentang alur pengiriman formulir pada umumnya:
- Pengguna mengisi formulir.
- Pengguna mengklik "Kirim".
- Klien mengirim data ke endpoint API server.
- Server memproses data, memvalidasinya, dan melakukan tindakan (misalnya, menyimpan ke database).
- Server mengirimkan respons kembali (misalnya, pesan sukses atau daftar kesalahan validasi).
- Kode sisi klien harus mengurai respons ini dan memperbarui UI sesuai dengan itu.
Secara tradisional, ini membutuhkan pengelolaan state loading, state error, dan state sukses secara manual. useFormState menyederhanakan seluruh proses ini, terutama saat digunakan dengan Server Actions dalam framework seperti Next.js. Ini menciptakan hubungan langsung dan deklaratif antara pengiriman formulir dan state yang dihasilkannya.
Keuntungan paling signifikan adalah penyempurnaan progresif. Sebuah formulir yang dibangun dengan useFormState dan server action akan berfungsi sempurna bahkan jika JavaScript dinonaktifkan. Browser akan melakukan pengiriman halaman penuh, server action akan berjalan, dan server akan me-render halaman berikutnya dengan state yang dihasilkan (misalnya, menampilkan kesalahan validasi). Ketika JavaScript diaktifkan, React mengambil alih, mencegah muat ulang halaman penuh, dan memberikan pengalaman aplikasi satu halaman (SPA) yang mulus. Anda mendapatkan yang terbaik dari kedua dunia dengan satu basis kode.
Memahami Dasar-dasar: `useFormState` vs. `useState`
Untuk memahami useFormState, akan sangat membantu jika membandingkannya dengan hook useState yang sudah dikenal. Meskipun keduanya mengelola state, mekanisme pembaruan dan kasus penggunaan utamanya berbeda.
Tanda tangan (signature) untuk useFormState adalah:
const [state, formAction] = useFormState(fn, initialState);
Menguraikan Tanda Tangan:
fn: Fungsi yang akan dipanggil saat formulir dikirimkan. Ini biasanya adalah server action. Fungsi ini menerima dua argumen: state sebelumnya dan data formulir. Nilai yang dikembalikannya menjadi state yang baru.initialState: Nilai yang Anda inginkan untuk state pada awalnya, sebelum formulir pernah dikirimkan.state: State saat ini dari formulir. Pada render awal, nilainya adalahinitialState. Setelah pengiriman formulir, nilainya menjadi nilai kembalian dari fungsi action Andafn.formAction: Action baru yang Anda teruskan ke propactionpada elemen<form>Anda. Ketika action ini dipanggil (saat pengiriman formulir), ia akan memanggil fungsi asli Andafndan memperbarui state.
Perbandingan Konseptual
Bayangkan sebuah penghitung sederhana.
Dengan useState, Anda mengelola pembaruan sendiri:
const [count, setCount] = useState(0);
function handleIncrement() {
setCount(c => c + 1);
}
Di sini, handleIncrement adalah event handler yang secara eksplisit memanggil state setter.
Dengan useFormState, pembaruan state adalah hasil dari sebuah action:
// Ini adalah contoh sederhana, bukan server action, untuk ilustrasi
function incrementAction(previousState, formData) {
// formData akan berisi data kiriman jika ini adalah form sungguhan
return previousState + 1;
}
const [count, dispatchIncrement] = useFormState(incrementAction, 0);
// Anda akan menggunakan `dispatchIncrement` di prop action sebuah form.
Perbedaan utamanya adalah useFormState dirancang untuk alur pembaruan state yang asinkron dan berbasis hasil, yang persis seperti yang terjadi selama pengiriman formulir ke server. Anda tidak memanggil fungsi `setState`; Anda mengirimkan sebuah action, dan hook memperbarui state dengan nilai kembalian dari action tersebut.
Implementasi Praktis: Membangun Form Pertama Anda dengan `useFormState`
Mari beralih dari teori ke praktik. Kita akan membangun formulir pendaftaran buletin sederhana yang menunjukkan fungsionalitas inti dari useFormState. Contoh ini mengasumsikan pengaturan dengan framework yang mendukung React Server Actions, seperti Next.js dengan App Router.
Langkah 1: Definisikan Server Action
Server action adalah fungsi yang dapat Anda tandai dengan direktif 'use server';. Ini memungkinkan fungsi tersebut dieksekusi dengan aman di server, bahkan ketika dipanggil dari komponen klien. Ini adalah pasangan yang sempurna untuk useFormState.
Mari kita buat sebuah file, misalnya, app/actions.js:
'use server';
// Ini adalah action yang disederhanakan. Di aplikasi sungguhan, Anda akan memvalidasi email
// dan menyimpannya ke database atau layanan pihak ketiga.
export async function subscribeToNewsletter(previousState, formData) {
const email = formData.get('email');
// Validasi dasar sisi server
if (!email || !email.includes('@')) {
return {
message: 'Silakan masukkan alamat email yang valid.',
success: false
};
}
console.log(`Pelanggan baru: ${email}`);
// Mensimulasikan penyimpanan ke database
await new Promise(res => setTimeout(res, 1000));
return {
message: 'Terima kasih telah berlangganan!',
success: true
};
}
Perhatikan tanda tangan fungsinya: (previousState, formData). Ini diperlukan untuk fungsi yang digunakan dengan useFormState. Kami memeriksa email dan mengembalikan objek terstruktur yang akan menjadi state baru komponen kami.
Langkah 2: Buat Komponen Form
Sekarang, mari kita buat komponen sisi klien yang menggunakan action ini.
'use client';
import { useFormState } from 'react-dom';
import { subscribeToNewsletter } from './actions';
const initialState = {
message: null,
success: false,
};
export function NewsletterForm() {
const [state, formAction] = useFormState(subscribeToNewsletter, initialState);
return (
<div>
<h3>Gabung Buletin Kami</h3>
<form action={formAction}>
<label htmlFor="email">Alamat Email:</label>
<input type="email" id="email" name="email" required />
<button type="submit">Berlangganan</button>
</form>
{state.message && (
<p style={{ color: state.success ? 'green' : 'red' }}>
{state.message}
</p>
)}
</div>
);
}
Membedah Komponen:
- Kita mengimpor
useFormStatedarireact-dom. Ini penting—hook ini tidak ada di paket intireact. - Kita mendefinisikan objek
initialState. Ini memastikan variabelstatekita terdefinisi dengan baik pada render pertama. - Kita memanggil
useFormState(subscribeToNewsletter, initialState)untuk mendapatkanstatedanformActionyang sudah dibungkus. - Kita meneruskan
formActionini langsung ke propactionpada elemen<form>. Inilah koneksi ajaibnya. - Kita me-render pesan secara kondisional berdasarkan
state.message, dengan gaya yang berbeda untuk kasus sukses dan error.
Sekarang, ketika pengguna mengirimkan formulir, hal berikut terjadi:
- React mencegat pengiriman.
- Ia memanggil server action
subscribeToNewsletterdengan state saat ini dan data formulir. - Server action berjalan, menjalankan logikanya, dan mengembalikan objek state baru.
useFormStatemenerima objek baru ini dan memicu re-render pada komponenNewsletterFormdenganstateyang diperbarui.- Pesan sukses atau error muncul di bawah formulir, tanpa muat ulang halaman penuh.
Validasi Form Tingkat Lanjut dengan `useFormState`
Contoh sebelumnya menunjukkan pesan sederhana. Kekuatan sebenarnya dari useFormState bersinar saat menangani kesalahan validasi yang kompleks dan spesifik untuk setiap field yang dikembalikan dari server.
Langkah 1: Tingkatkan Server Action untuk Error yang Rinci
Mari kita buat action formulir pendaftaran yang lebih tangguh. Ini akan memvalidasi username, email, dan password, mengembalikan objek error di mana kunci-kuncinya sesuai dengan nama field.
Di app/actions.js:
'use server';
export async function registerUser(previousState, formData) {
const username = formData.get('username');
const email = formData.get('email');
const password = formData.get('password');
const errors = {};
if (!username || username.length < 3) {
errors.username = 'Username harus terdiri dari minimal 3 karakter.';
}
if (!email || !email.includes('@')) {
errors.email = 'Harap berikan alamat email yang valid.';
} else if (await isEmailTaken(email)) { // Mensimulasikan pengecekan ke database
errors.email = 'Email ini sudah terdaftar.';
}
if (!password || password.length < 8) {
errors.password = 'Password harus terdiri dari minimal 8 karakter.';
}
if (Object.keys(errors).length > 0) {
return { errors };
}
// Lanjutkan dengan registrasi pengguna...
console.log('Mendaftarkan pengguna:', { username, email });
return { message: 'Pendaftaran berhasil! Silakan periksa email Anda untuk verifikasi.' };
}
// Fungsi pembantu untuk mensimulasikan pencarian ke database
async function isEmailTaken(email) {
if (email === 'test@example.com') {
return true;
}
return false;
}
Action kita sekarang mengembalikan objek state yang dapat memiliki salah satu dari dua bentuk: { errors: { ... } } atau { message: '...' }.
Langkah 2: Bangun Form untuk Menampilkan Error Spesifik Field
Komponen klien sekarang perlu membaca objek error terstruktur ini dan menampilkan pesan di sebelah input yang relevan.
'use client';
import { useFormState } from 'react-dom';
import { registerUser } from './actions';
const initialState = {
message: null,
errors: {},
};
export function RegistrationForm() {
const [state, formAction] = useFormState(registerUser, initialState);
return (
<form action={formAction}>
<h2>Buat Akun</h2>
{state?.message && <p className="success-message">{state.message}</p>}
<div className="form-group">
<label htmlFor="username">Username</label>
<input id="username" name="username" aria-describedby="username-error" />
{state?.errors?.username && (
<p id="username-error" className="error-message">{state.errors.username}</p>
)}
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" aria-describedby="email-error" />
{state?.errors?.email && (
<p id="email-error" className="error-message">{state.errors.email}</p>
)}
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" aria-describedby="password-error" />
{state?.errors?.password && (
<p id="password-error" className="error-message">{state.errors.password}</p>
)}
</div>
<button type="submit">Daftar</button>
</form>
);
}
Catatan Aksesibilitas: Kita menggunakan atribut aria-describedby pada input, yang menunjuk ke ID dari kontainer pesan error. Ini sangat penting bagi pengguna pembaca layar, karena secara terprogram menghubungkan field input dengan kesalahan validasi spesifiknya.
Menggabungkan dengan Validasi Sisi Klien
Validasi sisi server adalah sumber kebenaran, tetapi menunggu bolak-balik ke server untuk memberitahu pengguna bahwa mereka melewatkan '@' di email mereka adalah pengalaman yang buruk. useFormState tidak menggantikan validasi sisi klien; ia melengkapinya dengan sempurna.
Anda dapat menambahkan atribut validasi HTML5 standar untuk umpan balik instan:
<input
id="username"
name="username"
required
minLength="3"
aria-describedby="username-error"
/>
<input
id="email"
name="email"
type="email"
required
aria-describedby="email-error"
/>
Dengan ini, browser akan mencegah pengiriman formulir jika aturan dasar sisi klien ini tidak terpenuhi. Alur useFormState hanya akan berjalan untuk data sisi klien yang valid, di mana ia melakukan pemeriksaan sisi server yang lebih kompleks dan aman (seperti apakah email sudah digunakan).
Mengelola State UI yang Tertunda dengan `useFormStatus`
Ketika sebuah formulir dikirimkan, ada jeda saat server action sedang dieksekusi. Pengalaman pengguna yang baik melibatkan pemberian umpan balik selama waktu ini, misalnya, dengan menonaktifkan tombol kirim dan menampilkan indikator pemuatan.
React menyediakan hook pendamping untuk tujuan ini: useFormStatus.
Hook useFormStatus memberikan informasi status tentang pengiriman formulir terakhir. Yang terpenting, ia harus dirender di dalam komponen <form> yang statusnya ingin Anda lacak.
Membuat Tombol Kirim yang Cerdas
Merupakan praktik terbaik untuk membuat komponen terpisah untuk tombol kirim Anda yang menggunakan hook ini.
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Mengirim...' : 'Daftar'}
</button>
);
}
Sekarang, kita bisa mengimpor dan menggunakan SubmitButton ini di RegistrationForm kita:
// ... di dalam komponen RegistrationForm
import { SubmitButton } from './SubmitButton';
// ...
<SubmitButton />
</form>
// ...
Ketika pengguna mengklik tombol, hal berikut terjadi:
- Pengiriman formulir dimulai.
- Hook
useFormStatusdi dalamSubmitButtonmelaporkanpending: true. - Komponen
SubmitButtonmelakukan re-render. Tombol menjadi nonaktif dan teksnya berubah menjadi "Mengirim...". - Setelah server action selesai dan
useFormStatememperbarui state, formulir tidak lagi dalam status pending. useFormStatusmelaporkanpending: false, dan tombol kembali ke keadaan normal.
Pola sederhana ini secara drastis meningkatkan pengalaman pengguna dengan memberikan umpan balik yang jelas dan segera mengenai status formulir.
Praktik Terbaik dan Kesalahan Umum
Saat Anda mengintegrasikan useFormState ke dalam proyek Anda, perhatikan pedoman ini untuk menghindari masalah umum.
Yang Harus Dilakukan
- Sediakan
initialStateyang terdefinisi dengan baik. Ini mencegah error pada render awal ketika properti state Anda (sepertierrors) mungkin tidak terdefinisi. - Jaga agar bentuk state Anda konsisten. Selalu kembalikan objek dengan kunci yang sama dari action Anda (misalnya,
message,errors), bahkan jika nilainya null atau kosong. Ini membuat logika rendering sisi klien Anda lebih sederhana. - Gunakan
useFormStatusuntuk umpan balik UX. Tombol yang dinonaktifkan selama pengiriman tidak dapat ditawar untuk pengalaman pengguna yang profesional. - Prioritaskan aksesibilitas. Gunakan tag
label, dan hubungkan pesan error ke input denganaria-describedby. - Kembalikan objek state baru. Dalam server action Anda, selalu kembalikan objek baru. Jangan memutasi argumen
previousState.
Yang Tidak Boleh Dilakukan
- Jangan lupakan argumen pertama. Fungsi action Anda harus menerima
previousStatesebagai argumen pertamanya, bahkan jika Anda tidak menggunakannya. - Jangan memanggil
useFormStatusdi luar<form>. Itu tidak akan berhasil. Ia harus menjadi turunan dari formulir yang sedang dipantaunya. - Jangan tinggalkan validasi sisi klien. Gunakan atribut HTML5 atau library ringan untuk umpan balik instan pada batasan sederhana. Andalkan server untuk validasi logika bisnis dan keamanan.
- Jangan letakkan logika sensitif di komponen formulir. Keindahan pola ini adalah semua logika validasi dan pemrosesan data kritis Anda berada dengan aman di server dalam action.
Kapan Memilih `useFormState` Dibandingkan Library Lain
React memiliki ekosistem library formulir yang kaya. Jadi, kapan Anda harus menggunakan useFormState bawaan versus library seperti React Hook Form atau Formik?
Pilih `useFormState` ketika:
- Anda menggunakan framework modern yang berpusat pada server. Ini dirancang untuk bekerja dengan Server Actions dalam framework seperti Next.js (App Router), Remix, dll.
- Penyempurnaan progresif adalah prioritas. Jika Anda membutuhkan formulir Anda untuk berfungsi tanpa JavaScript, ini adalah solusi bawaan terbaik di kelasnya.
- Validasi Anda sangat bergantung pada server. Untuk formulir di mana aturan validasi terpenting memerlukan pencarian database atau logika bisnis yang kompleks,
useFormStateadalah pilihan yang alami. - Anda ingin meminimalkan JavaScript sisi klien. Pola ini memindahkan manajemen state dan logika validasi ke server, menghasilkan bundle klien yang lebih ringan.
Pertimbangkan library lain (seperti React Hook Form) ketika:
- Anda membangun SPA tradisional. Jika aplikasi Anda adalah aplikasi yang di-render di Sisi Klien (CSR) yang berkomunikasi dengan API REST atau GraphQL, library sisi klien yang berdedikasi seringkali lebih ergonomis.
- Anda membutuhkan interaktivitas sisi klien yang sangat kompleks. Untuk fitur seperti validasi real-time yang rumit, wizard multi-langkah dengan state klien bersama, array field dinamis, atau transformasi data yang kompleks sebelum pengiriman, library yang matang menawarkan lebih banyak utilitas siap pakai.
- Kinerja sangat penting untuk formulir yang sangat besar. Library seperti React Hook Form dioptimalkan untuk meminimalkan re-render di klien, yang dapat bermanfaat untuk formulir dengan puluhan atau ratusan field.
Pilihan ini tidak saling eksklusif. Dalam aplikasi besar, Anda mungkin menggunakan useFormState untuk formulir sederhana yang terikat ke server (seperti formulir kontak atau pendaftaran) dan library berfitur lengkap untuk dasbor pengaturan kompleks yang murni interaktif di sisi klien.
Kesimpulan: Masa Depan Formulir di React
Hook useFormState lebih dari sekadar API baru; ini adalah cerminan dari filosofi React yang terus berkembang. Dengan mengintegrasikan state formulir secara erat dengan action sisi server, ia menjembatani kesenjangan klien-server dengan cara yang terasa kuat dan sederhana.
Dengan memanfaatkan hook ini, Anda mendapatkan tiga keuntungan penting:
- Manajemen State yang Disederhanakan: Anda menghilangkan boilerplate untuk mengambil data secara manual, menangani state loading, dan mengurai respons server.
- Ketangguhan Secara Bawaan: Penyempurnaan progresif sudah tertanam, memastikan formulir Anda dapat diakses dan fungsional untuk semua pengguna, terlepas dari perangkat atau kondisi jaringan mereka.
- Pemisahan Kepentingan yang Jelas: Logika UI tetap berada di komponen klien Anda, sementara logika bisnis dan validasi ditempatkan secara aman di server.
Seiring ekosistem React terus mengadopsi pola yang berpusat pada server, menguasai useFormState dan pendampingnya useFormStatus akan menjadi keterampilan penting bagi para developer yang ingin membangun aplikasi web modern, tangguh, dan ramah pengguna. Ini mendorong kita untuk membangun untuk web sebagaimana mestinya—tangguh dan dapat diakses—sambil tetap memberikan pengalaman yang kaya dan interaktif yang diharapkan oleh pengguna.